iT邦幫忙

2026 iThome 鐵人賽

DAY 16
1
Software Development

從 C++ 菜鳥到 Low-Latency 勇者:一場分秒必爭的賽局系列 第 16

[Day 16] High-Performance Concurrency: Lock-Free Programming I

  • 分享至 

  • xImage
  •  

在 Low-Latency C++ 的設計中,最大的困難並非只有程式速度的提升,而是在高併發及高負載下仍然維持可預期穩定的延遲。對金融交易、即時行情、遊戲引擎、網路服務、訊息佇列與高效能資料處理系統而言,P99 的 latency 往往才是決定維運品質的關鍵因素。

歷史悠久的 std::mutex 是最直觀的同步工具,但 mutex 的成本不僅僅是執行一次 lock/unlock 所需要的 CPU cycles。當競爭發生時,thread 可能被阻塞、觸發 context switch,甚至進入 kernel。這些行為會造成 latency spike,使原本幾微秒的操作突然變成數十甚至數百微秒。因此,在極端低延遲場景中,lock-free programming 成為一項重要技術。

1. Lock-Free 到底解決了什麼?

Lock-free 的核心概念,是透過 atomic operation 建立多執行緒間的同步,而不是讓 thread 互相等待 mutex

傳統 mutex 不只是速度慢,其可能造成:

  • Blocking:拿不到 lock 的 thread 必須等待。
  • Context Switch:thread 被 suspend / wake up 可能產生額外成本。
  • Lock Contention:大量 thread 搶同一把 lock
  • Priority Inversion:低優先權 thread 持有 lock,高優先權 thread 反而被卡住。
  • Deadlock:多把 lock 使用不當可能互相等待。

Thread AㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤThread B
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ ㅤ│
ㅤㅤ├── lock ───────┐ㅤ│
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ │ㅤ│
ㅤㅤ│ㅤㅤ修改 shared data ㅤㅤ│ㅤ├── lock
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ │ㅤ│
ㅤㅤ├── unlock ──────┘ㅤ│
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ ㅤ├── 被阻塞
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ │
ㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ ㅤ└── 等待 A

以下是一個最基本的例子。這個寫法利用 CPU 提供的 atomic instruction,不需要 mutex 就可以安全地讓多個 thread 同時修改 counter

std::atomic<int> counter{0};
counter.fetch_add(1, std::memory_order_relaxed);

lock();     // No need.
counter++;  // No need.
unlock();   // No need.

當然,使用 std::atomic 並不代表程式自動變成高效能。真正重要的是理解 atomicity、memory ordering、cache coherence 與資料結構設計之間的關係。

上述例子也可以改成 memory_order_seq_cst。雖然語意簡單,但可能引入不必要的 ordering constraint。若這個 counter 只是統計事件數量,而其他 thread 並不依賴 counter 來判斷資料是否已經準備完成,那麼 memory_order_relaxed 通常已經足夠。

2. Lock-Free 到底保障了什麼?

低延遲程式設計的原則是:不要盲目使用最強的 synchronization guarantee,而應該選擇足夠正確的 memory ordering。而 lock-free 是 progress guarantee,即使某一個 thread 停住,其他 thread 仍然可以繼續完成操作,但不代表每個 thread 都不會等待

順便介紹 guarantee 常見的三個層級:

  • Obstruction-Free:沒有其他 thread 干擾時,最終可以完成。
  • Lock-Free:整體系統保證持續有某個 operation 完成。
  • Wait-Free:每一個 thread 都保證在有限步驟內完成。

3. Lock-Free 的靈魂 —— Memory Ordering

C++ atomic 提供多種 memory order,包含昨天的文章已有提到的 Acquire-Release Semantics。以下再簡單說明幾種最常用的方式:

  • relaxed:只保證 atomic operation 本身是原子的,不提供跨變數的 ordering。
  • release:發布資料,確保之前的 memory operation 不會被重新排序到 release 之後。
  • acquire:取得資料,確保之後的 memory operation 不會被重新排序到 acquire 之前。
  • seq_cst:提供最強的全域順序保證。

典型的 producer-consumer pattern:

std::atomic<bool> ready{false};
Data data;

void producer()
{
    data.value = 42;
    ready.store(true, std::memory_order_release);
}

void consumer()
{
    while (!ready.load(std::memory_order_acquire))
        ;

    std::cout << data.value;
}

這裡的重點不是 ready 本身,而是 release/acquire 建立的 happens-before relationship。Producer 先寫入 data.value,再進行 release store;consumer 透過 acquire load 看到 ready == true 後,就可以安全觀察 producer 在 release 之前完成的寫入。

值得注意的是,ready 這個 atomic operation 並不會建立一般 memory access 的 synchronization,不能單純因為 ready == true 就推論另一個 thread 一定能正確看到 data.value == 42。如果錯誤地改成兩邊都是 relaxed,atomic flag 依然是安全的,但不能因此推導出 data.value 的同步關係。

由此可見,lock-free programming 最危險的部份並非 syntax,而是考驗工程師對 memory model 的理解是否深入。


上一篇
[Day 15] High-Performance Concurrency: Memory Models & Ordering
下一篇
[Day 17] High-Performance Concurrency: Lock-Free Programming II
系列文
從 C++ 菜鳥到 Low-Latency 勇者:一場分秒必爭的賽局21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言